docker-spoon 0.8.0 → 1.0.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.
@@ -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