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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rspec +2 -0
- data/Gemfile.lock +5 -8
- data/README.md +112 -112
- data/README_config.md +248 -0
- data/Rakefile +7 -17
- data/bin/spoon +0 -2
- data/docker-spoon.gemspec +1 -2
- data/features/build.feature +49 -0
- data/features/create_list_destroy.feature +81 -0
- data/features/debug.feature +25 -0
- data/features/docker_host_env.feature +24 -0
- data/features/help.feature +67 -0
- data/features/list-images.feature +17 -0
- data/features/portforwards.feature +94 -0
- data/features/ports.feature +126 -0
- data/features/privileged_mode.feature +55 -0
- data/features/step_definitions/spoon_steps.rb +0 -1
- data/features/support/env.rb +24 -1
- data/lib/spoon.rb +461 -304
- data/lib/spoon/version.rb +1 -1
- data/spec/spec_helper.rb +17 -0
- data/spec/template_spec.rb +20 -0
- metadata +28 -22
- data/features/spoon.feature +0 -24
- data/test/tc_something.rb +0 -7
@@ -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
|
data/features/support/env.rb
CHANGED
@@ -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
|
data/lib/spoon.rb
CHANGED
@@ -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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
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
|
-
|
84
|
+
def self.parse(args)
|
85
|
+
config = {}
|
86
|
+
optparser = OptionParser.new do |opts|
|
35
87
|
|
36
|
-
|
37
|
-
|
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
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
87
|
-
|
88
|
-
|
117
|
+
opts.on("--builddir DIR", "Directory containing Dockerfile") do |b|
|
118
|
+
config[:builddir] = b
|
119
|
+
end
|
89
120
|
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
133
|
+
opts.on("--prefix PREFIX", "Prefix for container names") do |prefix|
|
134
|
+
config[:prefix] = prefix
|
135
|
+
end
|
104
136
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
end
|
137
|
+
opts.on("--privileged", "Enable privileged mode for new containers") do |privileged|
|
138
|
+
config[:privileged] = true
|
139
|
+
end
|
109
140
|
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
-
|
130
|
-
|
131
|
-
|
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
|
-
|
137
|
-
|
138
|
-
|
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
|
-
|
141
|
-
|
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
|
-
|
151
|
-
|
152
|
-
instance_start(container)
|
200
|
+
def self.remove_prefix(name)
|
201
|
+
name.sub(/\/?#{@options[:prefix]}/, '')
|
153
202
|
end
|
154
203
|
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
-
|
160
|
-
|
161
|
-
|
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
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
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
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
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
|
-
|
190
|
-
|
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
|
-
|
200
|
-
|
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
|
-
|
271
|
+
puts "Connecting to `#{name}`"
|
272
|
+
instance_ssh(name, command)
|
273
|
+
end
|
203
274
|
|
204
|
-
|
205
|
-
|
206
|
-
puts "
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
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
|
-
|
218
|
-
|
219
|
-
|
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
|
-
|
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
|
-
|
233
|
-
|
234
|
-
|
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
|
-
|
309
|
+
return false
|
240
310
|
end
|
241
|
-
else
|
242
|
-
puts "No container named: #{name}"
|
243
311
|
end
|
244
|
-
end
|
245
312
|
|
246
|
-
|
247
|
-
|
248
|
-
|
313
|
+
def self.instance_network(name)
|
314
|
+
docker_url
|
315
|
+
|
316
|
+
container = get_container(name)
|
249
317
|
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
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
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
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
|
-
|
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
|
-
|
284
|
-
|
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
|
-
|
297
|
-
|
298
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
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
|
-
|
319
|
-
|
320
|
-
|
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
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
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
|
-
|
332
|
-
|
333
|
-
|
435
|
+
def self.get_all_containers
|
436
|
+
::Docker::Container.all(:all => true)
|
437
|
+
end
|
334
438
|
|
335
|
-
|
336
|
-
|
337
|
-
|
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
|
-
|
340
|
-
|
341
|
-
|
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
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
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
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
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
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
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
|
-
|
377
|
-
|
378
|
-
|
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
|
-
|
381
|
-
|
382
|
-
|
532
|
+
def self.docker_url
|
533
|
+
::Docker.url = @options[:url]
|
534
|
+
end
|
383
535
|
|
384
|
-
|
385
|
-
|
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
|
-
|
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
|