docker-spoon 0.8.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,55 @@
1
+ @build @clean
2
+ Feature: Test privileged mode
3
+
4
+ Scenario: Privileged mode defined in config file
5
+
6
+ Given a file named "spoon_config" with:
7
+ """
8
+ options[:url] = "tcp://127.0.0.1:2375"
9
+ options[:image] = "spoon_test"
10
+ options[:privileged] = true
11
+ """
12
+
13
+ When I run `spoon -c spoon_config testcontainer19919 --nologin`
14
+ Then the exit status should be 0
15
+
16
+ When I run `docker inspect spoon-testcontainer19919`
17
+ Then the output should contain:
18
+ """
19
+ "Privileged": true,
20
+ """
21
+
22
+ Scenario: Privileged mode defined on command line
23
+
24
+ Given a file named "spoon_config" with:
25
+ """
26
+ options[:url] = "tcp://127.0.0.1:2375"
27
+ options[:image] = "spoon_test"
28
+ options[:privileged] = false
29
+ """
30
+
31
+ When I run `spoon -c spoon_config testcontainer19919 --nologin --privileged`
32
+ Then the exit status should be 0
33
+
34
+ When I run `docker inspect spoon-testcontainer19919`
35
+ Then the output should contain:
36
+ """
37
+ "Privileged": true,
38
+ """
39
+
40
+ Scenario: Privileged mode not defined
41
+
42
+ Given a file named "spoon_config" with:
43
+ """
44
+ options[:url] = "tcp://127.0.0.1:2375"
45
+ options[:image] = "spoon_test"
46
+ """
47
+
48
+ When I run `spoon -c spoon_config testcontainer19919 --nologin`
49
+ Then the exit status should be 0
50
+
51
+ When I run `docker inspect spoon-testcontainer19919`
52
+ Then the output should contain:
53
+ """
54
+ "Privileged": false,
55
+ """
@@ -1 +0,0 @@
1
- # Put your step definitions here
@@ -1,10 +1,13 @@
1
1
  require 'aruba/cucumber'
2
- require 'methadone/cucumber'
3
2
 
4
3
  ENV['PATH'] = "#{File.expand_path(File.dirname(__FILE__) + '/../../bin')}#{File::PATH_SEPARATOR}#{ENV['PATH']}"
5
4
  LIB_DIR = File.join(File.expand_path(File.dirname(__FILE__)),'..','..','lib')
5
+ C_NAME = "testcontainer19919"
6
+ URL = "tcp://127.0.0.1:2375"
7
+ IMAGE = "spoon_test"
6
8
 
7
9
  Before do
10
+ @aruba_timeout_seconds = 10
8
11
  # Using "announce" causes massive warnings on 1.9.2
9
12
  @puts = true
10
13
  @original_rubylib = ENV['RUBYLIB']
@@ -14,3 +17,23 @@ end
14
17
  After do
15
18
  ENV['RUBYLIB'] = @original_rubylib
16
19
  end
20
+
21
+ Before('@clean') do
22
+ run_simple("spoon --url #{URL} -d #{C_NAME} --force")
23
+ end
24
+
25
+ After('@clean') do
26
+ run_simple("spoon --url #{URL} -d #{C_NAME} --force")
27
+ end
28
+
29
+ Before('@docker_env') do
30
+ ENV['DOCKER_HOST'] = URL
31
+ end
32
+
33
+ Before('@build') do
34
+ run_simple("spoon --url #{URL} --image #{IMAGE} --build --builddir ../../docker")
35
+ end
36
+
37
+ Before('@interact') do
38
+ @aruba_io_wait_seconds = 5
39
+ end
@@ -3,389 +3,546 @@ require 'docker'
3
3
  require 'json'
4
4
  require 'uri'
5
5
  require 'rainbow'
6
+ require 'optparse'
6
7
 
7
8
  module Spoon
8
- include Methadone::Main
9
- include Methadone::CLILogging
10
- include Methadone::SH
11
- version(Spoon::VERSION)
12
-
13
- main do |instance|
14
-
15
- D options.inspect
16
- if options[:list]
17
- instance_list
18
- elsif options["list-images"]
19
- image_list
20
- elsif options[:build]
21
- image_build
22
- elsif options[:destroy]
23
- instance_destroy(apply_prefix(options[:destroy]))
24
- elsif options[:network]
25
- instance_network(apply_prefix(options[:network]))
26
- elsif instance
27
- instance_connect(apply_prefix(instance), options[:command])
28
- else
29
- help_now!("You either need to provide an action or an instance to connect to")
9
+ class Client
10
+
11
+ attr_accessor :options
12
+ @options = {}
13
+
14
+ # This combines our default configs with our command line and config file
15
+ # configurations in the desired precedence
16
+ def self.combine_config
17
+ config = self.parse(ARGV)
18
+
19
+ # init config only options
20
+ @options["pre-build-commands"] = []
21
+ @options[:copy_on_create] = []
22
+ @options[:run_on_create] = []
23
+ @options[:add_authorized_keys] = false
24
+ @options[:command] = ''
25
+
26
+ # init command line options
27
+ @options[:builddir] = '.'
28
+ @options[:url] = ::Docker.url
29
+ @options[:image] = 'spoon-pairing'
30
+ @options[:prefix] = 'spoon-'
31
+ @options[:privileged] ||= false
32
+
33
+ # Eval config file
34
+ D "Config file is: #{config[:config]}"
35
+ options = {}
36
+ if File.exists?(config[:config])
37
+ eval(File.read(config[:config]))
38
+ else
39
+ puts "File #{config[:config]} does not exist"
40
+ exit(1)
41
+ end
42
+
43
+ # Read in config file values
44
+ options.each do |k, v|
45
+ @options[k] = v
46
+ end
47
+
48
+ # Read in command line values
49
+ config.each do |k, v|
50
+ @options[k] = v
51
+ end
52
+
53
+ @options
30
54
  end
31
55
 
32
- end
56
+ def self.main
57
+
58
+ instance = false
59
+ @options = combine_config
60
+ instance = ARGV[0]
61
+ D @options.inspect
62
+ if @options[:list]
63
+ instance_list
64
+ elsif @options["list-images"]
65
+ image_list
66
+ elsif @options[:build]
67
+ image_build
68
+ elsif @options[:destroy]
69
+ instance_destroy(apply_prefix(@options[:destroy]))
70
+ elsif @options[:kill]
71
+ instance_kill(apply_prefix(@options[:kill]))
72
+ elsif @options[:restart]
73
+ instance_restart(apply_prefix(@options[:restart]))
74
+ elsif @options[:network]
75
+ instance_network(apply_prefix(@options[:network]))
76
+ elsif instance
77
+ instance_connect(apply_prefix(instance), @options[:command])
78
+ else
79
+ puts("You either need to provide an action or an instance to connect to")
80
+ exit
81
+ end
82
+ end
33
83
 
34
- description "Create & Connect to pairing environments in Docker"
84
+ def self.parse(args)
85
+ config = {}
86
+ optparser = OptionParser.new do |opts|
35
87
 
36
- # Actions
37
- on("-l", "--list", "List available spoon instances")
38
- on("-d", "--destroy NAME", "Destroy spoon instance with NAME")
39
- on("-b", "--build", "Build image from Dockerfile using name passed to --image")
40
- on("-n", "--network NAME", "Display exposed ports using name passed to NAME")
88
+ opts.banner = "Usage: spoon [options] [instance name]\n\n"
89
+ opts.banner += "Create & Connect to pairing environments in Docker\n\n"
41
90
 
42
- # Configurables
43
- options[:config] ||= "#{ENV['HOME']}/.spoonrc"
44
- on("-c FILE", "--config", "Config file to use for spoon options")
91
+ opts.program_name = "spoon"
45
92
 
46
- # Read config file & set options
47
- if File.exists?(options[:config])
48
- eval(File.open(options[:config]).read)
49
- end
93
+ opts.on("-l", "--list", "List available spoon instances") do
94
+ config[:list] = true
95
+ end
96
+ opts.on("-d", "--destroy NAME", "Destroy spoon instance with NAME") do |destroy|
97
+ config[:destroy] = destroy
98
+ end
99
+ opts.on("-b", "--build", "Build image from Dockerfile using name passed to --image") do
100
+ config[:build] = true
101
+ end
102
+ opts.on("-n", "--network NAME", "Display exposed ports using name passed to NAME") do |name|
103
+ config[:network] = name
104
+ end
105
+ opts.on("--restart NAME", "Restart the specified spoon instance") do |name|
106
+ config[:restart] = name
107
+ end
108
+ opts.on("--kill NAME", "Kill the specified spoon instance") do |name|
109
+ config[:kill] = name
110
+ end
50
111
 
51
- options[:builddir] ||= '.'
52
- on("--builddir DIR", "Directory containing Dockerfile")
53
- on("--pre-build-commands", "List of commands to run locally before building image")
54
- # These are config only options
55
- options[:copy_on_create] ||= []
56
- options[:run_on_create] ||= []
57
- options[:add_authorized_keys] ||= false
58
- options[:url] ||= Docker.url
59
- on("-u", "--url URL", "Docker url to connect to")
60
- on("-L", "--list-images", "List available spoon images")
61
- options[:image] ||= "spoon-pairing"
62
- on("-i", "--image NAME", "Use image for spoon instance")
63
- options[:prefix] ||= 'spoon-'
64
- on("-p", "--prefix PREFIX", "Prefix for container names")
65
- options[:command] ||= ''
66
- on("-f", "--force", "Skip any confirmations")
67
- on("--debug", "Enable debug")
68
- on("--debugssh", "Enable SSH debugging")
69
- on("-P PORT", "--portforwards", "Forward PORT over ssh (must be > 1023)")
70
-
71
-
72
- arg(:instance, :optional, "Spoon instance to connect to")
73
-
74
- use_log_level_option
75
-
76
- def self.confirm_delete?(name)
77
- if options[:force]
78
- return true
79
- else
80
- print "Are you sure you want to delete #{name}? (y/n) "
81
- answer = $stdin.gets.chomp.downcase
82
- return answer == "y"
83
- end
84
- end
112
+ config[:config] = "#{ENV['HOME']}/.spoonrc"
113
+ opts.on("-c", "--config FILE", "Config file to use for spoon @options") do |c|
114
+ config[:config] = c
115
+ end
85
116
 
86
- def self.apply_prefix(name)
87
- "#{options[:prefix]}#{name}"
88
- end
117
+ opts.on("--builddir DIR", "Directory containing Dockerfile") do |b|
118
+ config[:builddir] = b
119
+ end
89
120
 
90
- def self.remove_prefix(name)
91
- name.gsub(/\/?#{options[:prefix]}/, '')
92
- end
121
+ opts.on("--url URL", "Docker url to connect to") do |url|
122
+ config[:url] = url
123
+ end
124
+
125
+ opts.on("--list-images", "List available spoon images") do
126
+ config["list-images"] = true
127
+ end
93
128
 
94
- def self.image_build
95
- # Run pre-build commands
96
- options["pre-build-commands"].each do |command|
97
- sh command
98
- end unless options["pre-build-commands"].nil?
99
- D "pre-build commands complete, building Docker image"
129
+ opts.on("--image NAME", "Use image for spoon instance") do |image|
130
+ config[:image] = image
131
+ end
100
132
 
101
- docker_url
102
- build_opts = { 't' => options[:image], 'rm' => true }
103
- docker_connection = ::Docker::Connection.new(options[:url], :read_timeout => 3000)
133
+ opts.on("--prefix PREFIX", "Prefix for container names") do |prefix|
134
+ config[:prefix] = prefix
135
+ end
104
136
 
105
- Docker::Image.build_from_dir(options[:builddir], build_opts, docker_connection) do |chunk|
106
- print_docker_response(chunk)
107
- end
108
- end
137
+ opts.on("--privileged", "Enable privileged mode for new containers") do |privileged|
138
+ config[:privileged] = true
139
+ end
109
140
 
110
- def self.image_list
111
- docker_url
112
- Docker::Image.all.each do |image|
113
- next if image.info["RepoTags"] == ["<none>:<none>"]
114
- puts "Image: #{image.info["RepoTags"]}"
115
- end
116
- end
141
+ opts.on("--force", "Skip any confirmations") do
142
+ config[:force] = true
143
+ end
117
144
 
118
- def self.print_parsed_response(response)
119
- case response
120
- when Hash
121
- response.each do |key, value|
122
- case key
123
- when 'stream'
124
- puts value
125
- else
126
- puts "#{key}: #{value}"
145
+ opts.on("--nologin", "Do not ssh to contianer, just create") do
146
+ config[:nologin] = true
147
+ end
148
+
149
+ opts.on("--debug", "Enable debug") do
150
+ config[:debug] = true
151
+ end
152
+
153
+ opts.on("--debugssh", "Enable SSH debugging") do
154
+ config[:debugssh] = true
155
+ end
156
+
157
+ opts.on("--ports PORT", Array, "Expose additional docker ports") do |ports|
158
+ config[:ports] = ports
159
+ end
160
+
161
+ opts.on("--portforwards PORT", Array, "Forward PORT over ssh (must be > 1023)") do |portforwards|
162
+ config[:portforwards] = portforwards
163
+ end
164
+
165
+ opts.on("--version", "Show version") do
166
+ puts Spoon::VERSION
167
+ exit
168
+ end
169
+
170
+ opts.on("-h", "--help", "Show help") do
171
+ puts opts
172
+ exit
127
173
  end
128
174
  end
129
- when Array
130
- response.each do |hash|
131
- print_parsed_response(hash)
175
+
176
+ begin
177
+ optparser.parse!(ARGV)
178
+ rescue OptionParser::MissingArgument, OptionParser::InvalidOption
179
+ puts $!.to_s
180
+ puts optparser
181
+ exit(1)
132
182
  end
183
+ config
133
184
  end
134
- end
135
185
 
136
- def self.print_docker_response(json)
137
- print_parsed_response(JSON.parse(json))
138
- end
186
+ def self.confirm_delete?(name)
187
+ if @options[:force]
188
+ return true
189
+ else
190
+ print "Are you sure you want to delete #{name}? (y/n) "
191
+ answer = $stdin.gets.chomp.downcase
192
+ return answer == "y"
193
+ end
194
+ end
139
195
 
140
- def self.instance_connect(name, command='')
141
- docker_url
142
- if not instance_exists? name
143
- puts "The `#{name}` container doesn't exist, creating..."
144
- instance_create(name)
145
- instance_copy_authorized_keys(name, options[:add_authorized_keys])
146
- instance_copy_files(name)
147
- instance_run_actions(name)
196
+ def self.apply_prefix(name)
197
+ "#{@options[:prefix]}#{name}"
148
198
  end
149
199
 
150
- container = get_container(name)
151
- unless is_running?(container)
152
- instance_start(container)
200
+ def self.remove_prefix(name)
201
+ name.sub(/\/?#{@options[:prefix]}/, '')
153
202
  end
154
203
 
155
- puts "Connecting to `#{name}`"
156
- instance_ssh(name, command)
157
- end
204
+ def self.image_build
205
+ # Run pre-build commands
206
+ @options["pre-build-commands"].each do |command|
207
+ sh command
208
+ end unless @options["pre-build-commands"].nil?
209
+ D "pre-build commands complete, building Docker image"
210
+
211
+ docker_url
212
+ build_opts = { 't' => @options[:image], 'rm' => true }
213
+ docker_connection = ::Docker::Connection.new(@options[:url], :read_timeout => 3000)
214
+
215
+ # Quick sanity check for Dockerfile
216
+ unless File.exist?("#{@options[:builddir]}/Dockerfile")
217
+ puts "Directory `#{@options[:builddir]}` must contain a Dockerfile... cannot continue"
218
+ exit(1)
219
+ end
158
220
 
159
- def self.instance_list
160
- docker_url
161
- puts "List of available spoon containers:"
162
- container_list = get_spoon_containers
163
- if container_list.empty?
164
- puts "No spoon containers running at #{options[:url]}"
165
- exit
166
- end
167
- max_width_container_name = remove_prefix(container_list.max_by {|c| c.info["Names"].first.to_s.length }.info["Names"].first.to_s)
168
- max_name_width = max_width_container_name.length
169
- container_list.each do |container|
170
- name = container.info["Names"].first.to_s
171
- running = is_running?(container) ? Rainbow("Running").green : Rainbow("Stopped").red
172
- puts "#{remove_prefix(name)} [ #{running} ]".rjust(max_name_width + 22) + " " + Rainbow(image_name(container)).yellow
221
+ ::Docker::Image.build_from_dir(@options[:builddir], build_opts, docker_connection) do |chunk|
222
+ print_docker_response(chunk)
223
+ end
173
224
  end
174
- end
175
225
 
176
- def self.image_name(container)
177
- env = Hash[container.json['Config']['Env'].collect { |v| v.split('=') }]
178
- return env['IMAGE_NAME'] || container.json['Config']['Image'].split(':').first
179
- end
226
+ def self.image_list
227
+ docker_url
228
+ ::Docker::Image.all.each do |image|
229
+ next if image.info["RepoTags"] == ["<none>:<none>"]
230
+ puts "Image: #{image.info["RepoTags"]}"
231
+ end
232
+ end
180
233
 
181
- def self.strip_slash(name)
182
- if name.start_with? "/"
183
- name[1..-1]
184
- else
185
- name
234
+ def self.print_parsed_response(response)
235
+ case response
236
+ when Hash
237
+ response.each do |key, value|
238
+ case key
239
+ when 'stream'
240
+ puts value
241
+ else
242
+ puts "#{key}: #{value}"
243
+ end
244
+ end
245
+ when Array
246
+ response.each do |hash|
247
+ print_parsed_response(hash)
248
+ end
249
+ end
186
250
  end
187
- end
188
251
 
189
- def self.is_running?(container)
190
- container = Docker::Container.get(container.info["id"])
191
- status = container.info["State"]["Running"] || nil
192
- unless status.nil?
193
- return status
194
- else
195
- return false
252
+ def self.print_docker_response(json)
253
+ print_parsed_response(JSON.parse(json))
196
254
  end
197
- end
198
255
 
199
- def self.instance_network(name)
200
- docker_url
256
+ def self.instance_connect(name, command='')
257
+ docker_url
258
+ if not instance_exists? name
259
+ puts "The '#{name}' container doesn't exist, creating..."
260
+ instance_create(name)
261
+ instance_copy_authorized_keys(name, @options[:add_authorized_keys])
262
+ instance_copy_files(name)
263
+ instance_run_actions(name)
264
+ end
265
+
266
+ container = get_container(name)
267
+ unless is_running?(container)
268
+ instance_start(container)
269
+ end
201
270
 
202
- container = get_container(name)
271
+ puts "Connecting to `#{name}`"
272
+ instance_ssh(name, command)
273
+ end
203
274
 
204
- if is_running?(container)
205
- host = URI.parse(options[:url]).host
206
- puts "Host: #{host}"
207
- ports = container.json['NetworkSettings']['Ports']
208
- ports.each do |p_name, p_port|
209
- tcp_name = p_name.split('/')[0]
210
- puts "#{tcp_name} -> #{p_port.first['HostPort']}"
275
+ def self.instance_list
276
+ docker_url
277
+ puts "List of available spoon containers:"
278
+ container_list = get_spoon_containers
279
+ if container_list.empty?
280
+ puts "No spoon containers running at #{@options[:url]}"
281
+ exit
282
+ end
283
+ max_width_container_name = remove_prefix(container_list.max_by {|c| c.info["Names"].first.to_s.length }.info["Names"].first.to_s)
284
+ max_name_width = max_width_container_name.length
285
+ container_list.each do |container|
286
+ name = container.info["Names"].first.to_s
287
+ running = is_running?(container) ? Rainbow("Running").green : Rainbow("Stopped").red
288
+ puts "#{remove_prefix(name)} [ #{running} ]".rjust(max_name_width + 22) + " " + Rainbow(image_name(container)).yellow
211
289
  end
212
- else
213
- puts "Container is not running, cannot show ports"
214
290
  end
215
- end
216
291
 
217
- def self.instance_destroy(name)
218
- docker_url
219
- container = get_container(name)
220
-
221
- if container
222
- if confirm_delete?(name)
223
- puts "Destroying #{name}"
224
- begin
225
- container.kill
226
- rescue
227
- puts "Failed to kill container #{container.id}"
228
- end
292
+ def self.image_name(container)
293
+ env = Hash[container.json['Config']['Env'].collect { |v| v.split('=') }]
294
+ return env['IMAGE_NAME'] || container.json['Config']['Image']
295
+ end
229
296
 
230
- container.wait(10)
297
+ def self.strip_slash(name)
298
+ if name.start_with? "/"
299
+ name[1..-1]
300
+ else
301
+ name
302
+ end
303
+ end
231
304
 
232
- begin
233
- container.delete(:force => true)
234
- rescue
235
- puts "Failed to remove container #{container.id}"
236
- end
237
- puts "Done!"
305
+ def self.is_running?(container)
306
+ if /^Up.+/ =~ container.info["Status"]
307
+ return $~
238
308
  else
239
- puts "Delete aborted.. #{name} lives to pair another day."
309
+ return false
240
310
  end
241
- else
242
- puts "No container named: #{name}"
243
311
  end
244
- end
245
312
 
246
- def self.instance_exists?(name)
247
- get_container(name)
248
- end
313
+ def self.instance_network(name)
314
+ docker_url
315
+
316
+ container = get_container(name)
249
317
 
250
- def self.get_port_forwards(forwards = "")
251
- if options[:portforwards]
252
- options[:portforwards].split.each do |port|
253
- (lport,rport) = port.split(':')
254
- forwards << "-L #{lport}:127.0.0.1:#{rport || lport} "
318
+ if is_running?(container)
319
+ host = URI.parse(@options[:url]).host
320
+ puts "Host: #{host}"
321
+ ports = container.json['NetworkSettings']['Ports']
322
+ ports.each do |p_name, p_port|
323
+ tcp_name = p_name.split('/')[0]
324
+ puts "#{tcp_name} -> #{p_port.first['HostPort']}"
325
+ end
326
+ else
327
+ puts "Container is not running, cannot show ports"
255
328
  end
256
329
  end
257
- return forwards
258
- end
259
330
 
260
- def self.instance_ssh(name, command='', exec=true)
261
- container = get_container(name)
262
- forwards = get_port_forwards
263
- D "Got forwards: #{forwards}"
264
- host = URI.parse(options[:url]).host
265
- if container
266
- ssh_command = "\"#{command}\"" if not command.empty?
267
- ssh_port = get_port('22', container)
268
- puts "Waiting for #{name}:#{ssh_port}..." until host_available?(host, ssh_port)
269
- ssh_options = "-t -o StrictHostKeyChecking=no -p #{ssh_port} #{forwards} "
270
- ssh_options << "-v " if options[:debugssh]
271
- ssh_cmd = "ssh #{ssh_options} pairing@#{host} #{ssh_command}"
272
- D "SSH CMD: #{ssh_cmd}"
273
- if exec
274
- exec(ssh_cmd)
331
+ def self.instance_destroy(name)
332
+ docker_url
333
+ container = get_container(name)
334
+
335
+ if container
336
+ if confirm_delete?(name)
337
+ puts "Destroying #{name}"
338
+ begin
339
+ container.kill
340
+ rescue
341
+ puts "Failed to kill container #{container.id}"
342
+ end
343
+
344
+ container.wait(10)
345
+
346
+ begin
347
+ container.delete(:force => true)
348
+ rescue
349
+ puts "Failed to remove container #{container.id}"
350
+ end
351
+ puts "Done!"
352
+ else
353
+ puts "Delete aborted.. #{name} lives to pair another day."
354
+ end
275
355
  else
276
- system(ssh_cmd)
356
+ puts "No container named: #{name}"
277
357
  end
278
- else
279
- puts "No container named: #{container.inspect}"
280
358
  end
281
- end
282
359
 
283
- def self.instance_copy_authorized_keys(name, keyfile)
284
- D "Setting up authorized_keys file"
285
- # We sleep this once to cope w/ slow starting ssh daemon on create
286
- sleep 1
287
- if keyfile
288
- full_keyfile = "#{ENV['HOME']}/.ssh/#{keyfile}"
289
- key = File.read(full_keyfile).chop
290
- D "Read keyfile `#{full_keyfile}` with contents:\n#{key}"
291
- cmd = "mkdir -p .ssh ; chmod 700 .ssh ; echo '#{key}' >> .ssh/authorized_keys"
292
- instance_ssh(name, cmd, false)
360
+ def self.instance_exists?(name)
361
+ get_container(name)
293
362
  end
294
- end
295
363
 
296
- def self.instance_copy_files(name)
297
- options[:copy_on_create].each do |file|
298
- D "Copying file #{file}"
364
+ def self.get_port_forwards(forwards = "")
365
+ if @options[:portforwards]
366
+ @options[:portforwards].each do |port|
367
+ (lport,rport) = port.split(':')
368
+ forwards << "-L #{lport}:127.0.0.1:#{rport || lport} "
369
+ end
370
+ end
371
+ return forwards
372
+ end
373
+
374
+ def self.instance_ssh(name, command='', exec=true)
299
375
  container = get_container(name)
300
- host = URI.parse(options[:url]).host
376
+ forwards = get_port_forwards
377
+ D "Got forwards: #{forwards}"
378
+ host = URI.parse(@options[:url]).host
301
379
  if container
380
+ ssh_command = "\"#{command}\"" if not command.empty?
302
381
  ssh_port = get_port('22', container)
303
382
  puts "Waiting for #{name}:#{ssh_port}..." until host_available?(host, ssh_port)
304
- system("scp -o StrictHostKeyChecking=no -P #{ssh_port} #{ENV['HOME']}/#{file} pairing@#{host}:#{file}")
383
+ ssh_options = "-t -o StrictHostKeyChecking=no -p #{ssh_port} #{forwards} "
384
+ ssh_options << "-v " if @options[:debugssh]
385
+ ssh_cmd = "ssh #{ssh_options} pairing@#{host} #{ssh_command}"
386
+ puts "SSH Forwards: #{forwards}" unless forwards.empty?
387
+ D "SSH CMD: #{ssh_cmd}"
388
+ return if @options[:nologin]
389
+ if exec
390
+ exec(ssh_cmd)
391
+ else
392
+ system(ssh_cmd)
393
+ end
305
394
  else
306
395
  puts "No container named: #{container.inspect}"
307
396
  end
308
397
  end
309
- end
310
398
 
311
- def self.instance_run_actions(name)
312
- options[:run_on_create].each do |action|
313
- puts "Running command: #{action}"
314
- instance_ssh(name, action, false)
399
+ def self.instance_copy_authorized_keys(name, keyfile)
400
+ D "Setting up authorized_keys file"
401
+ # We sleep this once to cope w/ slow starting ssh daemon on create
402
+ sleep 1
403
+ if keyfile
404
+ full_keyfile = "#{ENV['HOME']}/.ssh/#{keyfile}"
405
+ key = File.read(full_keyfile).chop
406
+ D "Read keyfile `#{full_keyfile}` with contents:\n#{key}"
407
+ cmd = "mkdir -p .ssh ; chmod 700 .ssh ; echo '#{key}' >> .ssh/authorized_keys"
408
+ instance_ssh(name, cmd, false)
409
+ end
315
410
  end
316
- end
317
411
 
318
- def self.get_all_containers
319
- Docker::Container.all(:all => true)
320
- end
412
+ def self.instance_copy_files(name)
413
+ @options[:copy_on_create].each do |file|
414
+ D "Copying file #{file}"
415
+ container = get_container(name)
416
+ host = URI.parse(@options[:url]).host
417
+ if container
418
+ ssh_port = get_port('22', container)
419
+ puts "Waiting for #{name}:#{ssh_port}..." until host_available?(host, ssh_port)
420
+ return if @options[:nologin]
421
+ system("scp -o StrictHostKeyChecking=no -P #{ssh_port} #{ENV['HOME']}/#{file} pairing@#{host}:#{file}")
422
+ else
423
+ puts "No container named: #{container.inspect}"
424
+ end
425
+ end
426
+ end
321
427
 
322
- def self.get_spoon_containers
323
- container_list = get_all_containers.select { |c| c.info["Names"].first.to_s.start_with? "/#{options[:prefix]}" }
324
- unless container_list.empty?
325
- return container_list.sort { |c1, c2| c1.info["Names"].first.to_s <=> c2.info["Names"].first.to_s }
326
- else
327
- return container_list
428
+ def self.instance_run_actions(name)
429
+ @options[:run_on_create].each do |action|
430
+ puts "Running command: #{action}"
431
+ instance_ssh(name, action, false)
432
+ end
328
433
  end
329
- end
330
434
 
331
- def self.get_running_containers
332
- Docker::Container.all
333
- end
435
+ def self.get_all_containers
436
+ ::Docker::Container.all(:all => true)
437
+ end
334
438
 
335
- def self.instance_start(container)
336
- container.start!
337
- end
439
+ def self.get_spoon_containers
440
+ container_list = get_all_containers.select { |c| c.info["Names"].first.to_s.start_with? "/#{@options[:prefix]}" }
441
+ unless container_list.empty?
442
+ return container_list.sort { |c1, c2| c1.info["Names"].first.to_s <=> c2.info["Names"].first.to_s }
443
+ else
444
+ return container_list
445
+ end
446
+ end
338
447
 
339
- def self.get_container(name)
340
- docker_url
341
- container_list = get_all_containers
448
+ def self.get_running_containers
449
+ ::Docker::Container.all
450
+ end
451
+
452
+ def self.instance_start(container)
453
+ container.start!
454
+ end
455
+
456
+ def self.instance_restart(name)
457
+ container = get_container(name)
458
+ container.kill
459
+ container.start!
460
+ puts "Container #{name} restarted"
461
+ end
462
+
463
+ def self.instance_kill(name)
464
+ container = get_container(name)
465
+ container.kill
466
+ puts "Container #{name} killed"
467
+ end
342
468
 
343
- l_name = strip_slash(name)
344
- container_list.each do |container|
345
- if container.info["Names"].first.to_s == "/#{l_name}"
346
- return container
469
+ def self.get_container(name)
470
+ docker_url
471
+ container_list = get_all_containers
472
+
473
+ l_name = strip_slash(name)
474
+ container_list.each do |container|
475
+ if container.info["Names"].first.to_s == "/#{l_name}"
476
+ return container
477
+ end
347
478
  end
479
+ return nil
348
480
  end
349
- return nil
350
- end
351
481
 
352
- def self.instance_create(name)
353
- docker_url
354
- container = Docker::Container.create({
355
- 'Image' => options[:image],
356
- 'name' => name,
357
- 'Entrypoint' => 'runit',
358
- 'Hostname' => remove_prefix(name)
359
- })
360
- container = container.start({ 'PublishAllPorts' => true })
361
- end
482
+ def self.container_config(name)
483
+ data = {
484
+ :Cmd => 'runit',
485
+ :Image => @options[:image],
486
+ :AttachStdout => true,
487
+ :AttachStderr => true,
488
+ :Privileged => @options[:privileged],
489
+ :PublishAllPorts => true,
490
+ :Tty => true
491
+ }
492
+ # Yes, this key must be a string
493
+ data['name'] = name
494
+ data[:CpuShares] = @options[:cpu] if @options[:cpu]
495
+ data[:Dns] = @options[:dns] if @options[:dns]
496
+ data[:Hostname] = remove_prefix(name)
497
+ data[:Memory] = @options[:memory] if @options[:memory]
498
+ ports = ['22'] + Array(@options[:ports]).map { |mapping| mapping.to_s }
499
+ ports.compact!
500
+ data[:PortSpecs] = ports
501
+ data[:PortBindings] = ports.inject({}) do |bindings, mapping|
502
+ guest_port, host_port = mapping.split(':').reverse
503
+ bindings["#{guest_port}/tcp"] = [{
504
+ :HostIp => '',
505
+ :HostPort => host_port || ''
506
+ }]
507
+ bindings
508
+ end
509
+ data[:Volumes] = Hash[Array(@options[:volume]).map { |volume| [volume, {}] }]
510
+ data
511
+ end
362
512
 
363
- def self.host_available?(hostname, port)
364
- socket = TCPSocket.new(hostname, port)
365
- IO.select([socket], nil, nil, 5)
366
- rescue SocketError, Errno::ECONNREFUSED,
367
- Errno::EHOSTUNREACH, Errno::ENETUNREACH, IOError
368
- sleep(0.25)
369
- false
370
- rescue Errno::EPERM, Errno::ETIMEDOUT
371
- false
372
- ensure
373
- socket && socket.close
374
- end
513
+ def self.instance_create(name)
514
+ docker_url
515
+ container = ::Docker::Container.create(container_config(name))
516
+ container = container.start(container_config(name))
517
+ end
375
518
 
376
- def self.docker_url
377
- Docker.url = options[:url]
378
- end
519
+ def self.host_available?(hostname, port)
520
+ socket = TCPSocket.new(hostname, port)
521
+ IO.select([socket], nil, nil, 5)
522
+ rescue SocketError, Errno::ECONNREFUSED,
523
+ Errno::EHOSTUNREACH, Errno::ENETUNREACH, IOError
524
+ sleep(0.25)
525
+ false
526
+ rescue Errno::EPERM, Errno::ETIMEDOUT
527
+ false
528
+ ensure
529
+ socket && socket.close
530
+ end
379
531
 
380
- def self.get_port(port, container)
381
- container.json['NetworkSettings']['Ports']["#{port}/tcp"].first['HostPort']
382
- end
532
+ def self.docker_url
533
+ ::Docker.url = @options[:url]
534
+ end
383
535
 
384
- def self.D(message)
385
- if options[:debug]
386
- puts "D: #{message}"
536
+ def self.get_port(port, container)
537
+ container.json['NetworkSettings']['Ports']["#{port}/tcp"].first['HostPort']
387
538
  end
388
- end
389
539
 
390
- go!
540
+ def self.D(message)
541
+ if @options[:debug]
542
+ puts "D: #{message}"
543
+ end
544
+ end
545
+
546
+ main
547
+ end
391
548
  end